Repository navigation
🔒 全面加固跨世界 GM RPC、用户脚本、Sandbox 与 Agent 边界 - #1747
cyfung1031 wants to merge 248 commits into
Conversation
Bind USER_SCRIPT bootstrap data to the service-worker-issued document token, preserve sender provenance, and enforce per-execution GM capabilities across page RPC and native message paths. Harden captured collection operations and exact-match early startup handling.
说明原生方法捕获、USER_SCRIPT 来源校验、页面 RPC 句柄生命周期、脚本包装完整性和回调收尾约束,降低后续维护时误改安全契约的风险。
| SC_RANDOM_ESKEY: "305a6a49-2e72-48ae-bc6e-3e9517322d9b", | ||
| SC_RANDOM_FNKEY: "843078d2-403b-4ec0-a6e0-358488e135ec", |
There was a problem hiding this comment.
用途不一样。安全一点(多重防护)。 改同一个也可以
好。我处理一下 |
Merge origin/main (d8c1d79) into codex/main-world-security-refactor (053cc15). Conflict resolutions: - content/utils.ts trimScriptInfo: keep the allowlist projection from scriptscat#1766 (pickPageLoadScriptFields) as the base. Pass the PR's derived and cloned fields as overrides: scriptRevision, metadata, value and config. The page-side isPageScriptInfo requires scriptRevision. Drop the PR's explicit execution-token deletes, because the allowlist already excludes executionHandle, executionEnvTag and executionRunFlag. pageLoad attaches those after authentication. - content/page_load_contract.ts: extend the override type. Skip undefined overrides so an unset config stays absent, matching the own-key copy of the allowlist. - service_worker/runtime.ts pageLoad: keep the PR's ?? -1 default, because userScriptBootstraps stores tabId as a number. Gate the popup run-count emit on tabId > 0, which is the scriptscat#1783 behavior. - Tests: keep both sides' cases. The scriptscat#1766 exact-key producer test now also expects scriptRevision. Verification: - pnpm run typecheck, pnpm run lint - pnpm test -- --run: 389 files passed - pnpm run test:ci: one run had a timing failure in src/pages/install/useInstallData.test.ts (a file identical to main). A rerun passed all 389 files. - pnpm run test:runtime-smoke: passed - pnpm run test:e2e: 69 passed Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
|
冲突已解决 |
|
复现出几个非常严重不能接受的问题:SW回收和更新脚本后,不刷新页面,脚本就无法正常使用了,会报错:"page execution binding is invalid" 这个PR已经不是人力可以弄的了,太头疼了 Review:真实浏览器验证发现的问题验证基于当前 head 🔴 1. SW 被回收后,GM 和脚本注入出问题(阻塞)复现时直接启动 Chrome,不在 SW 上挂调试器,让它自然空闲回收(约 31–33 秒):
复现步骤:
这里其实是两个独立的问题。 1a.
问题在于键的顺序不固定:从 SW 重启后 修复:在 const sortKeys = <T extends Record<string, any> | undefined>(m: T): T =>
m && (Object.fromEntries(Object.keys(m).sort().map((k) => [k, m[k]])) as T);
script = { ...script, metadata: sortKeys(script.metadata), selfMetadata: sortKeys(script.selfMetadata) };更彻底的做法是:revision 只对规范化后的源输入求哈希(代码、metadata、匹配规则、资源,加上编译器版本),不再对编译产物求哈希。同时建议补一个单测,断言键顺序不同的同一个脚本得到相同的 revision。 1b. execution binding 只保存在 SW 内存里
建议把 handle 改成签名令牌:会话密钥放在 🔴 2. 脚本更新后,已打开页面的 GM 调用全部失败(阻塞)先打开页面让脚本执行,然后更新这个脚本,再在页面里调用 GM,会报 建议采用 1b 的签名令牌方案后不再整体撤销,策略改为:
这样既保留了 base 的使用体验,也满足「旧 wrapper 不能获得新权限」的目标。 🔴 3. 大对象 GM 读写的性能严重退化(阻塞)在浏览器里实测,对象有 2 万条记录:
原因是 修复:普通对象或数组不可能是 Map/Set,可以直接跳过这两次探测: const proto = Native.objectGetPrototypeOf(value);
const plain = proto === Object.prototype || proto === null || Native.arrayIsArray(value);
if (!plain) try { /* Map 探测 */ } catch {}
if (!plain) try { /* Set 探测 */ } catch {}在同一个进程里对比,复制 2 万条记录的对象:base 7–25ms,PR 356–534ms,改后 15–22ms。小对象从 28–46µs 降到 2µs。改后的输出与 PR 版完全一致,带 getter 的对象仍然会被拒绝。 更进一步: 🔴 4. NaN/±Infinity 既存不住,还会让整批值变更被丢弃(阻塞)
建议在存储规范化阶段把非有限数按 JSON 语义转成 5.
|
|
...看來還有一段路要走 |
Checklist / 检查清单
背景
本 PR 的说明仅以当前完整代码树为准:
scriptscat:main80854540dd1759e55ce79f541fcf582ba9c0a499对比 PR headcc195781d048b7961e6af7d14029ce739998c73c。以下内容描述这两个最终状态之间的净差异,不引用 PR 中间 commits 曾经采用、随后修改或回退的方案。相较
main,本 PR 主要加固 MAIN world、USER_SCRIPT、Service Worker、Offscreen/Sandbox 与 Agent/CAT API 的跨上下文身份、传输、数据和生命周期边界;同时补齐 early-start 同步状态、GM storage 持久化兼容语义,以及异步 GM/RPC 收尾行为。变更总览
main的净变化scripting使用新增的PageEventMessage,以每个 document 的随机eventFlag派生performanceCustomEvent名称;pageLoad、value/event、external API 与 GM RPC 共用该 keyed bridgewindow.message总线上;随机 key 只降低被动可观察性,不承担授权PageRpcRegistry与 Service Worker execution binding;Page RPC v2 wire 使用handle + sequence + api + params,canonical UUID/run flag/envTag/grant 由 Service Worker 根据 handle 与真实 sender 解析RequestSequenceWindow;重复序列和窗口外旧序列拒绝,合法的大幅前向 gap 可按固定成本推进窗口content的普通 GM RPC、bootstrap、value update 与 event callback 使用浏览器原生 ExtensionMessage / user-script connection;同步 DOM 节点辅助仍单独使用 CustomEvent 路径;专用 user-script listener 不可用时可走 document token 绑定的 extension-port compatibility pathmain的 Offscreen/EventPage ↔ SandboxWindowMessage数据通道改为一次性 Window bootstrap + privateMessagePort;parent 校验 exact iframe source、固定 marker 与 exactly one transferred port,随后移除 Window listener;port attachment 同时作为 readinesswindow.message,也不能被动看到后续 sandbox 内部 GM/lifecycle/value/event/skill payloadMessagePort方法和原生MessageEvent.datagetter,wire envelope 继续做 data-property/shape 校验Document、metadata,并记录 UUID/flag/scriptRevision;执行前用捕获的原生Function.prototype.toString和当前Document读取 metadata,再与 authoritative script revision 对齐Object.assign的普通赋值语义Object.prototypesetter、__proto__等页面可控原型行为影响内部权威状态更新main会把 early-startscriptInfo放入 page-visible bootstrap event;HEAD 的 event detail 只保留scriptFlag,同步 value/config/userConfig/resource snapshot 保留在 registered wrapper closure;execution binding 仍由 authoritative pageLoad 下发scriptRevision/execution binding,并保留 bootstrap 前脚本自身已完成的 set/delete keystorageName串行 DB commit、value delivery 与 registered snapshot refresh;shared storage 同批更新;value-only refresh 不推进 static code/resource revision,并有 dirty/repair 路径null;cycle 无效;top-level Function/Symbol/BigInt 继续使用 ScriptCat 的删除语义;getter/Proxy property read 在此 compatibility surface 可观察REncoded增加NaN、Infinity、-Infinity显式 tag;all-values 路径返回 null-prototype dictionary,并安全保留__proto__等 own keyGM.setValue/ batch/delete 以 Service Worker RPC 与 freshness barrier 完成为准,不再依赖 valueUpdate broadcast 来 resolve;legacy 同步 API 仍立即返回request.script.uuid覆盖/注入 script identity;conversation/task/attachment/OPFS/DOM monitor 的访问检查使用该 canonical identityscriptUuid访问另一脚本资源ownerScriptUuid并按 owner 检查;task CRUD/run/history 按 owner scope;task 使用 generation/revision;attachment read 需在调用脚本拥有的持久化 conversation 中可达WindowMessageConnect减少重复实现main的边界和兼容语义固定为可重复验证PR 范围
scriptscat:main@80854540dd1759e55ce79f541fcf582ba9c0a499codex/main-world-security-refactor@cc195781d048b7961e6af7d14029ce739998c73c实现考虑
scriptRevision表示 compiled artifact;closure capturedDocument与 SW execution binding 表示具体运行实例/transport identity,三者不互相替代request.script.uuid决定 owner;UI/extension-internal 调用与 script-scoped 调用继续走各自现有权限模型已知限制
PageEventMessage仍是 page-visible transport。随机eventFlag不是 bearer secret,也不能替代 broker/Service Worker authorization。PageEventMessage没有通用 ready buffer;当前入口在 eventFlag negotiation 前预取 authoritative pageLoad,但只在 bridge 建立后消费并发送页面 payload。未来新增更早发送的 caller 时仍需单独证明 ordering。MessagePort,parent 不会报告 verified readiness,也不会 replay background/scheduled script;当前没有 timeout-ready 路径。chrome.userScripts.update/registration failure 下,storage 已成功但下一次 wrapper snapshot 可暂时 dirty,后续 repair/registration mutation 会再尝试恢复。undefined表现可不同。建议审查重点
PageEventMessage上所有 privileged request 是否都经过 shape/clone、handle、grant、sequence 与真实 sender 校验scriptRevision、authoritative reconcile 和 bootstrap 前本地写入是否组成一致生命周期验证
scriptscat:main80854540dd1759e55ce79f541fcf582ba9c0a499→ PR headcc195781d048b7961e6af7d14029ce739998c73c;ahead 239 / behind 0;143 files;+16,266 / -2,000e2e/main-world-keyed-bridge.spec.ts、page_event_message.test.ts、page_rpc.test.ts覆盖 keyed bridge、message envelope 与 handle/grant/sequence policye2e/sandbox-message-port.spec.ts、sandbox_message_channel.test.ts、message_port_message.ts相关测试覆盖 source/marker/one-port bootstrap、listener removal、private traffic 与 prototype poisoningexample/tests/gm_storage_test.js与e2e/gm-api.spec.ts覆盖 clone/normalization、unsupported value、special object、non-finite number,以及 immediate / RPC-settled / reload-persisted 三阶段